package com.ivan.toolkit.common;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.OutputStream;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.Optional;

import javax.crypto.Cipher;
import javax.crypto.CipherInputStream;
import javax.crypto.CipherOutputStream;
import javax.crypto.KeyGenerator;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.Marshaller;
import javax.xml.bind.Unmarshaller;

import lombok.extern.log4j.Log4j2;

@Log4j2
public class Configs {
		
	public static File getDirectory() {
		return new File(
				Applications.getHome(), 
				Applications.getProperty("Applications.dirConfig", "config"));
	}
	
	private static Cipher getCipher(int mode) 
			throws NoSuchAlgorithmException, NoSuchPaddingException, InvalidKeyException {
		
		String privateKey = Securitys.privateKey();
		
        KeyGenerator kgen = KeyGenerator.getInstance("AES");     
        kgen.init(128, new SecureRandom(privateKey.getBytes()));     
        SecretKey secretKey = kgen.generateKey();             
        
        Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");     
        cipher.init(Cipher.DECRYPT_MODE, secretKey); 
        
        return cipher;
	}
	
	private static File getFile(File dir, Class<?> cls, boolean encrypted) {
		if(encrypted) {
			return new File(dir, cls.getSimpleName() + ".xml.encryped");
		} else {
			return new File(dir, cls.getSimpleName() + ".xml");
		}
	}
	
	private static File getFile(Class<?> cls, boolean encrypted) {
		return getFile(getDirectory(), cls, encrypted);
	}
	
	private static InputStream getInputStream(File file, boolean encrypted) throws InvalidKeyException, FileNotFoundException, NoSuchAlgorithmException, NoSuchPaddingException {
		if(encrypted) {
			return new CipherInputStream(new FileInputStream(file), getCipher(Cipher.DECRYPT_MODE));
		} else {
			return new FileInputStream(file);
		}
	}
	
	private static <T> T newInstance(Class<T> cls) {
		try {
			return cls.newInstance();
		} catch (InstantiationException | IllegalAccessException e1) {
			log.error("create new instance failed. class: " + cls.getName());
			return null;
		}
	}
	
	public static <T> Optional<T> loadx(Class<T> cls, File file, boolean encrypted) {
		try(InputStream in = getInputStream(file, encrypted)){
			
	        JAXBContext context = JAXBContext.newInstance(cls);   
	        Unmarshaller unmarshaller = context.createUnmarshaller();   
	        
	        return Optional.of(cls.cast(unmarshaller.unmarshal(in)));
			
		} catch (Exception e) {
			return Optional.empty();
		}
	}
	
	public static <T> T load(Class<T> cls, File file, boolean encrypted) {
		return loadx(cls, file, encrypted).orElse(newInstance(cls));		
	}
	
	public static <T> T load(Class<T> cls, String filename, boolean encrypted) {
		return load(cls, new File(filename), encrypted);	
	}
	
	public static <T> T load(Class<T> cls, boolean encrypted) {		
		return load(cls, getFile(cls, encrypted), encrypted);
	}
	
	private static OutputStream getOutputStream(File file, boolean encrypted) throws FileNotFoundException, InvalidKeyException, NoSuchAlgorithmException, NoSuchPaddingException {
		File parent = file.getParentFile();
		if(!parent.exists()) {
			parent.mkdirs();
		}
		if(encrypted) {
			return new CipherOutputStream(new FileOutputStream(file), getCipher(Cipher.ENCRYPT_MODE));
		} else {
			return new FileOutputStream(file);
		}
	}
	
	public static <T> void save(Object v, File file, boolean encrypted) {
		
		try (OutputStream out = getOutputStream(file, encrypted))
		{
			
			JAXBContext context = JAXBContext.newInstance(new Class[] {v.getClass()});   
	        Marshaller marshaller = context.createMarshaller();   
	        //marshaller.setProperty(Marshaller.JAXB_ENCODING,"utf-8");
	        marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);  
	        //marshaller.setProperty(Marshaller.JAXB_FRAGMENT, false);
	        marshaller.marshal(v, out);
			
		} catch (Exception e) {
			
			log.error("save config file failed. class: " + v.getClass().getName(), e);
			
		} 
	}
	
	public static <T> void save(Object v, String filename, boolean encrypted) {
		
		save(v, new File(filename), encrypted);

	}
	
	public static <T> void save(Object v, boolean encrypted) {
		
		save(v, getFile(v.getClass(), encrypted), encrypted);
	}
	
	public static <T> InstanceBuilder <T> at(Class<T> cls) {
		return new InstanceBuilder<T>(cls);
	}
	
	public static class Instance<T> {

		private Class<T> cls;
		
		private T instance;
		
		private boolean eventable = false;
		
		private boolean encrypted = false;
		
		public Instance(Class<T> cls) {
			super();
			this.cls = cls;
		}
		
		public File file() {
			File f = Configs.getFile(cls, encrypted);
			if(!f.exists()) {
				this.reload();
				this.set();
			}
			return f;
		}
		
		public synchronized T get() {
			if(instance == null) {
				instance = Configs.load(cls, this.encrypted);
			}
			return instance;
		}
		
		public synchronized void reload() {
			instance = Configs.load(cls, this.encrypted);
		}
		
		public synchronized void set(T ins) {
			Configs.save(ins, encrypted);
			instance = Configs.load(cls, this.encrypted);
			if(eventable) {
				Events.postSimple(instance);
			}
		}
		
		public synchronized void set() {
			set(instance);
		}
		
		public synchronized void update(File directory) {
			File file = Configs.getFile(directory, cls, encrypted);
			if(file.exists()) {
				Configs.loadx(cls, file, encrypted).ifPresent(t -> {
					this.set(t);
				});
			}
		}
		
	}
	
	public static class InstanceBuilder<T> {
		
		private Instance<T> instance = null;
		
		private InstanceBuilder(Class<T> cls) {
			instance = new Instance<T>(cls);
		}
		
		public InstanceBuilder<T> enableEvent() {
			instance.eventable = true;
			return this;
		}
		
		public InstanceBuilder<T> encrypted() {
			instance.encrypted = true;
			return this;
		}
		
		public Instance<T> build() {
			return instance;
		}
		
		public Instance<T> buildWithFile() {
			instance.file();
			return instance;
		}
	}
	
}
